有了昨天介紹 SQLDelight 的基礎之下,讓我們繼續來看看怎麼使用它。
如果你還沒看過上一篇的話,請點這裡:
https://ithelp.ithome.com.tw/articles/10304793
我們所定義的 Player.sq 除了會在 MyDBImpl 裡插入 create table 的 command 之外,也會在相對應的 package 下建立相對應的 object class 以及他的 query class,以我們的例子就是 HockeyPlayer 跟 PlayerQueries,不過點進去看以後應該會發現目前好像沒什麼特別的功能,如下:
// HockeyPlayer.kt
public data class HockeyPlayer(
  public val player_number: Long,
  public val full_name: String
) {
  public override fun toString(): String = """
  |HockeyPlayer [
  |  player_number: $player_number
  |  full_name: $full_name
  |]
  """.trimMargin()
}
// PlayerQueries.kt
public interface PlayerQueries : Transacter
而 MyDBImpl 裡也會有一小段相關程式碼如下:
private class PlayerQueriesImpl(
  private val database: MyDBImpl,
  private val driver: SqlDriver
) : TransacterImpl(driver), PlayerQueries
接下來回到我們一開始的問題,我們要怎麼在 SQLDelight 定義 function,讓我們可以動態的呼叫相對應的 function 來操作我們的 table 呢?
要回答這個問題還是需要回頭看我們的 Player.sq,既然 SQLDelight 有辦法理解 sql command,最有可能的方式就是在這邊寫更多的 sql command 並給它個名字變成可呼叫的 function,而在 SQLDelight 裡定義一個 function 只要用冒號結尾,並接著 sql command 就可以了,範例如下:
selectAll:
SELECT *
FROM hockeyPlayer;
insert:
INSERT INTO hockeyPlayer(player_number, full_name)
VALUES (?, ?);
insertFullPlayerObject:
INSERT INTO hockeyPlayer(player_number, full_name)
VALUES ?;
把以上三段 sql 加到 Player.sq 並成功的 compile 之後,就會發現原本空的 PlayerQueries 現在變得跟 Room 所定義的 interface 有點像了,如下:
public interface PlayerQueries : Transacter {
  public fun <T : Any> selectAll(mapper: (player_number: Long, full_name: String) -> T): Query<T>
  public fun selectAll(): Query<HockeyPlayer>
  public fun insert(player_number: Long, full_name: String): Unit
  public fun insertFullPlayerObject(hockeyPlayer: HockeyPlayer): Unit
}
而具體的實作依然是放在
MyDBImpl裡面,有興趣的大家可以自行看看~
現在的問題變成我們要怎麼使用呢?
這時候就是我們的 driver 登場的時候了!相信大家還記得這些 driver 是 platform dependent 的,所以我們可以運用之前所介紹的 expect/actual 技巧,定義如下:
// in src/commonMain
expect class DriverFactory {
  expect fun createDriver(): SqlDriver
}
fun createDatabase(driverFactory): MyDB {
    val driver = driverFactory.createDriver()
    val database = MyDB(driver)
    return database
}
// in src/androidMain
actual class DriverFactory(private val context: Context) {
  actual fun createDriver(): SqlDriver {
    return AndroidSqliteDriver(MyDB.Schema, context, "test.db") 
  }
}
// in src/iosMain
actual class DriverFactory {
  actual fun createDriver(): SqlDriver {
    return NativeSqliteDriver(MyDB.Schema, "test.db")
  }
}
接下來在各 platform 裡建立 driverFactory 後,呼叫 createDatabase 就可以拿到 MyDB 的實體囉,而透過 database.playerQueries 就可以拿到我們的 query class,有這個物件就可以呼叫我們所定義好的 sql command 囉,範例如下:
// init here, can move to di
val driver = driverFactory.createDriver()
val database = MyDB(driver)
val playerQueries: PlayerQueries = database.playerQueries
println(playerQueries.selectAll().executeAsList())
// Prints [HockeyPlayer(15, "Ryan Getzlaf")]
playerQueries.insert(player_number = 10, full_name = "Corey Perry")
println(playerQueries.selectAll().executeAsList())
// Prints [HockeyPlayer(15, "Ryan Getzlaf"), HockeyPlayer(10, "Corey Perry")]
val player = HockeyPlayer(10, "Ronald McDonald")
playerQueries.insertFullPlayerObject(player)
其實在 multi-platform 裡使用 sql 說起來有點小複雜,各 platform dependent 的程式以及 sql 跟 Kotlin 間的轉換,還有不少 interface 跟 implementation 的結構,但這個設計其實很漂亮,相信多試幾次後大家一定沒問題的!
https://github.com/cashapp/sqldelight/
https://cashapp.github.io/sqldelight/
https://cashapp.github.io/sqldelight/multiplatform_sqlite/